package com.sharing.backgroundmodule.service;

import com.sharing.backgroundmodule.dto.AgeDto;
import com.sharing.backgroundmodule.dto.AreaDto;
import com.sharing.backgroundmodule.dto.RetentionRate;
import com.sharing.backgroundmodule.mapper.UserStatisticsMapper;
import com.sharing.common.Result;
import jdk.jfr.events.ThrowablesEvent;
import org.springframework.beans.BeanInfoFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.print.attribute.standard.MediaSize;
import javax.swing.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author MingTian
 * @version 1.0
 * @description TODO
 * @date 2023/6/11 21:26
 */
@Service
public class UserStatisticsService {
    @Autowired
    private UserStatisticsMapper userStatisticsMapper;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 查询用户注册数的业务层方法
     * @return
     */
    public Result<Integer> selectUserRegistrationNumbers() {
        Integer totals = null;

        // 第一步：从 redis 缓存中查询
        String redisTotals = stringRedisTemplate.opsForValue().get("user_registration_numbers");
        if (redisTotals != null) {
            totals = Integer.valueOf(redisTotals);
            return new Result<>(0, totals);
        }

        // 第二步：若从 redis 中查询为空，则去数据库中查询
        // 从数据库中查询用户的注册总量
        totals = userStatisticsMapper.selectUserRegistrationNumbers();

        // 第三步：数据更新入 redis 中
        stringRedisTemplate.opsForValue().set("user_registration_numbers", String.valueOf(totals),1 , TimeUnit.MINUTES);

        return new Result<>(0, totals);
    }

    /**
     * 网站访问量的业务层方法
     * @param duration
     * @return
     */
    public Result<List<Long>> selectVisitsNumbers(Integer duration) {
        // 在 redis 中采用 hash 结构存储
        // key: visits_numbers
        // field: 日期
        // value: 网站访问量

        List<Long> res = new ArrayList<>(duration);

        // 获取今日日期
        LocalDate endDate = LocalDate.now();

        // 起始日期
        LocalDate beginDate = endDate.minusDays(duration - 1);

        // 获取键值对
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("visits_numbers");

        // LocalDate 转字符串的转化器
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        for (int i = 0; i < duration; i++) {
            LocalDate date = beginDate.plusDays(i);
            String dateString = date.format(formatter);
            String numbers = (String) entries.get(dateString);
            if (numbers == null) {
                // 当日网站没有被任何用户访问，那么在 redis 中不会生成对应的键值对
                res.set(i, 0L);
            } else {
                res.set(i, Long.valueOf(numbers));
            }
        }

        return new Result<>(0, res);
    }

    /**
     * 网站的单日用户登录量的业务层方法
     * 该功能与查询 “网站的单日访问量” 类似，区别在于对于 “网站的单日访问量” 只需要在 redis 中为每一天生成一个网站的计数器（最简单的 key-value 即可），
     * 在同一天每当一次请求发过来，为当天的计数器自增一即可。对于统计 “单日用户量”， 需要对用户去重（因为同一用户在同一天可能访问多次，只需记录一次即可），
     * 此时简单的 key-value 并不能满足该需求，为了满足去重，在 redis 中有如下两种方案：
     *      方案一：每一日，在 redis 中生成一个 set 集合，用于记录当日访问网站的用户id，该 set 集合的长度就是当日访问网站的用户量
     *      方案二：采用 redis 中的 HyperLogLog 数据结构，借用一定的统计学规律，粗略地得到每日的访问用户数
     *
     * 上述两种方案，对于用户量较少的网站，可采用方案一，数值精确，而对于用户量较大的网站，需要采用方案二来有效的降低内存的使用量，下面的业务层方法将采用方案二
     * @param duration 日期范围
     * @return
     */
    public Result<List<Long>> selectUserNumber(Integer duration) {
        // 生成结果集数组对象
        List<Long> res = new ArrayList<>();

        // 获取今日日期
        LocalDate endDate = LocalDate.now();

        // 起始日期
        LocalDate beginDate = endDate.minusDays(duration - 1);

        // LocalDate 转字符串的转化器
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        for (int i = 0; i < duration; i++) {
            LocalDate date = beginDate.plusDays(i);
            String dateString = date.format(formatter);
            res.add(stringRedisTemplate.opsForHyperLogLog().size("user_Numbers" + dateString));
        }

        return new Result<>(0, res);
    }

    /**
     * 用户作品发布情况的业务层方法
     * @param thresholds 作品数量临界值集合
     * @param duration 指定的时间范围，默认单位为：年，若 duration = 0 代表不限作品的发布时间
     * @return
     */
    public Result<List<Integer>> selectUserNumberByProduction(List<Integer> thresholds, Integer duration) {
        // 校验参数 —— thresholds
        if (thresholds == null) {
            return new Result<>(1, "thresholds 参数不能为空");
        }

        // 检验参数 —— duration
        if (duration == null || duration < 0) {
            return new Result<>(1, "duration 参数必须为自然数");
        }

        // 初始化结果集数组对象
        List<Integer> res = new ArrayList<>(thresholds.size() + 1);

        // 处理特殊情况
        // 特殊情况1 —— thresholds.size() == 0 即代表查询用户的注册数
        if (thresholds.size() == 0) {
            Result<Integer> userRegistrationNumbers = selectUserRegistrationNumbers();
            res.add(userRegistrationNumbers.getData());
            return new Result<>(0, res);
        }

        // 整理 thresholds，按升序排序
        thresholds.sort(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        });

        // 为方便sql查询
        thresholds.add(selectMaxProductionsNumber(duration).getData());

        // 处理 duration
        // 获取今日日期
        LocalDate endDate = LocalDate.now();
        LocalDate beginDate = endDate.minusYears(duration);

        // LocalDate 转字符串的转化器
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        // 生成对应字符串，作为数据库查询的参数
        String begin = beginDate.format(formatter);
        String end = endDate.format(formatter);

        // 从数据库查询每个用户在指定时间范围内的作品发布数量
        List<Integer> nums = userStatisticsMapper.selectUserNumberByProduction(thresholds, begin, end);

        // 更新 res 结果集
        for (int i = 0; i < nums.size(); i++) {
            for (int j = 0; j < thresholds.size(); j++) {
                if (nums.get(i) <= thresholds.get(j)) {
                    res.set(j, res.get(j) + 1);
                    break;
                }
            }
        }

        return new Result<>(0, res);
    }

    /**
     * 最大的作品发布数量
     * @param duration 指定的时间范围，默认单位为：年，若 duration = 0 代表不限作品的发布时间
     * @return
     */
    public Result<Integer> selectMaxProductionsNumber(Integer duration) {
        // 检验参数 —— duration
        if (duration == null || duration < 0) {
            return new Result<>(1, "duration 参数必须为自然数");
        }

        // 处理 duration
        // 获取今日日期
        LocalDate endDate = LocalDate.now();
        LocalDate beginDate = endDate.minusYears(duration);

        // LocalDate 转字符串的转化器
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        // 生成对应字符串，作为数据库查询的参数
        String begin = beginDate.format(formatter);
        String end = endDate.format(formatter);

        // 从数据库查询在指定时间范围内最大的作品发布数量
        Integer maxProductionsNumber = userStatisticsMapper.selectMaxProductionsNumber(begin, end);

        return new Result<>(0, maxProductionsNumber);
    }

    /**
     * 用户年龄分布情况的业务层方法
     * 由前端所传入的 thresholds 划分用户数量区间，统计每个用户在指定时间范围内注册的用户数量，最终统计每个区间的用户数
     * @param thresholds 用户数量临界值集合
     * @param duration 指定的时间范围
     * @return
     */
    public Result<List<Integer>> selectUserNumberByAge(List<Integer> thresholds, Integer duration) {
        // 校验参数 —— thresholds
        if (thresholds == null) {
            return new Result<>(1, "thresholds 参数不能为空");
        }

        // 检验参数 —— duration
        if (duration == null || duration < 0) {
            return new Result<>(1, "duration 参数必须为自然数");
        }

        // 初始化结果集数组对象
        List<Integer> res = new ArrayList<>(thresholds.size() + 1);

        // 处理特殊情况
        // 特殊情况1 —— thresholds.size() == 0 即代表查询用户的注册数
        if (thresholds.size() == 0) {
            Result<Integer> userRegistrationNumbers = selectUserRegistrationNumbers();
            res.add(userRegistrationNumbers.getData());
            return new Result<>(0, res);
        }

        // 整理 thresholds，按升序排序
        thresholds.sort(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        });

        // 为方便sql查询
        thresholds.add(Integer.MAX_VALUE);

        // 处理 duration
        // 获取今日日期
        LocalDate endDate = LocalDate.now();
        LocalDate beginDate = endDate.minusYears(duration);

        // LocalDate 转字符串的转化器
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        // 生成对应字符串，作为数据库查询的参数
        String begin = beginDate.format(formatter);
        String end = endDate.format(formatter);

        // 每个年龄对应的用户数
        List<AgeDto> ageDtos = userStatisticsMapper.selectUserNumberByAge(begin, end);


        for (AgeDto ageDto : ageDtos) {
            for (int i = 0; i < thresholds.size(); i++) {
                if (ageDto.getAge() <= thresholds.get(i)) {
                    res.set(i, res.get(i) + ageDto.getNums());
                }
            }
        }

        return new Result<>(0, res);
    }

    /**
     * 用户所属地区分布情况
     * @param duration 指定的时间范围
     * @return
     */
    public Result<List<AreaDto>> selectUserNumberByArea(Integer duration) {
        // 检验参数 —— duration
        if (duration == null || duration < 0) {
            return new Result<>(1, "duration 参数必须为自然数");
        }

        // 处理 duration
        // 获取今日日期
        LocalDate endDate = LocalDate.now();
        LocalDate beginDate = endDate.minusYears(duration);

        // LocalDate 转字符串的转化器
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        // 生成对应字符串，作为数据库查询的参数
        String begin = beginDate.format(formatter);
        String end = endDate.format(formatter);

        List<AreaDto> areaDtos = userStatisticsMapper.selectUserNumberByArea(begin, end);

        return new Result<>(0, areaDtos);
    }

    /**
     * 新增用户数的业务层方法
     * 对于该功能需求，有如下两种方案解决：
     *      方案一：在 mysql 的 user_information 表中记录着用户的注册信息（包括注册日期），通过 sql 语句查询当日的用户注册数即可得到
     *      方案二：在 redis 中生成一对 key-value 键值对，key 代表日期，value 代表当日用户的注册数，本质上就是每天一个计数器
     * 下面业务层方法将采用方案二，可以有效的降低数据库的访问量，并提高查询效率
     * @param duration 指定的时间范围
     * @return
     */
    public Result<List<Integer>> selectNewUserNumber(Integer duration) {
        // 检验参数 —— duration
        if (duration == null || duration < 0) {
            return new Result<>(1, "duration 参数必须为自然数");
        }

        List<Integer> res = new ArrayList<>(duration);

        // 获取今日日期
        LocalDate endDate = LocalDate.now();

        // 起始日期
        LocalDate beginDate = endDate.minusDays(duration - 1);

        // 获取键值对
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("new_user");

        // LocalDate 转字符串的转化器
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        for (int i = 0; i < duration; i++) {
            LocalDate date = beginDate.plusDays(i);
            String dateString = date.format(formatter);
            String numbers = (String) entries.get(dateString);
            if (numbers == null) {
                // 某日没有新增注册用户，那么 redis 中不会生成对应的键值对
                res.set(i, 0);
            } else {
                res.set(i, Integer.valueOf(numbers));
            }
        }

        return new Result<>(0, res);
    }

    /**
     * 用户留存率
     * @param end
     * @param duration
     * @return
     */
    public Result<List<RetentionRate>> selectRetentionRate(String end, Integer duration) {
        // 校验参数 —— endDate
        if (end == null) {
            return new Result<>(1, "endDate 不能为空");
        }

        // 检验参数 —— duration
        if (duration == null || duration < 0) {
            return new Result<>(1, "duration 参数必须为自然数");
        }

        // 处理 duration
        // LocalDate 转字符串的转化器
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        // 获取截止日期
        LocalDate endDate = LocalDate.parse(end, formatter);
        LocalDate beginDate = endDate.minusDays(duration - 1);

        // 生成对应字符串，作为数据库查询的参数
        String begin = beginDate.format(formatter);

        // 数据库查询留存率
        List<RetentionRate> retentionRates = userStatisticsMapper.selectRetentionRate(begin, end);

        return new Result<>(0, retentionRates);
    }
}
